Keep a late-arriving server-component boundary claimable and its slot range live - #2967
Merged
ryansolid merged 1 commit intoAug 4, 2026
Conversation
… range live Two halves of the same failure in the notes example: with a 2s delay in noteView, /notes/0 renders correctly and then the app stops responding — clicking another note changes the URL and fetches nothing, and New/Edit/home change the URL and nothing else. 1. documentStreaming -> boundaryMayArrive `_$HY.done` stopped meaning "the page is complete" once post-done swaps became held-until-claimed (solidjs#2964): a fragment settling after global hydration keeps its placeholder, fallback and template in place until its boundary registers as claimant, and the replay that follows is what delivers the boundary element. A boundary rendering in that window (a frames slot fill or lazy route module running after the root pass) read done as "never", mounted a fresh frame, and orphaned the markup the replay then delivered — visible but owned by nothing. Worse, the id was never claimed, so `intercept` answered every later call for that function with the document placeholder instead of fetching, and dynamic's equals-gate made each navigation a no-op. An unresolved `pl-*` placeholder now keeps the answer "not yet". So the waiter can no longer strand a region forever, a reveal that exhausts the page's deferred fragments releases waiters to mount fresh. 2. The claim scope wraps the insert CALL, not insert's accessor A slot fill claims under the producer's hydration keys, and claiming runs through runWithOwner — which clears `tracking` along with the owner. With the claim inside the accessor, the binding's first read was untracked, so it stayed reactive only by accident: when that read returned another accessor, insert re-read it and picked the dependency up. A <Loading> answering a still-pending streamed fragment returns its fallback NODES instead, leaving the effect with no dependency at all and the range permanently inert — the boundary's own resume still claimed the swapped-in server markup, so the region looked right, but nothing downstream ever re-rendered it. Claiming around the insert call makes the first evaluation the render effect's own compute: still under the producer's keys, but tracked. Tests: two cases in frames-late-boundary-client (held fragment still owes the element after done, including that the next call reaches the network; and the give-up path), plus a hydrate-config spec for the live slot range — that claim path bails out entirely without hydratable JSX, so the untracked window never opens in the plain client config. All three fail on the prior code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 70d0da6 The changes in this PR will be included in the next version bump. This PR includes changesets to release 9 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Merging this PR will not alter performance
Comparing Footnotes
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #2964. With a 2s delay added to
noteViewin the notes example,/notes/0renders correctly and then the app stops responding: clicking another note changes the URL and fetches nothing, and New/Edit/home change the URL and nothing else. Two independent causes, both in the frames client.1.
_$HY.doneis no longer "the page is complete"Once post-done swaps became held-until-claimed, a fragment settling after global hydration keeps its placeholder, fallback and template in place until its boundary registers as claimant — and the replay that follows is what delivers the boundary element.
documentStreaming()readdoneas "never", so a boundary rendering in that window (a frames slot fill, or a lazy route module running after the root pass) mounted a fresh frame and orphaned the markup the replay then delivered. Captured from a live run:The region is visible but owned by nothing, and because the id is never claimed,
interceptanswers every later call for that function with the document placeholder instead of fetching —dynamic's equals-gate then makes each navigation a no-op.boundaryMayArrive()replacesdocumentStreaming(): still streaming, or an unresolvedpl-*placeholder is in the page. So a waiter cannot strand a region forever, a reveal that exhausts the page's deferred fragments releases waiters to mount fresh.2. The claim scope severed the slot range's reactivity
A slot fill claims under the producer's hydration keys, and claiming runs through
runWithOwner— which clearstrackingalong with the owner. With the claim inside insert's accessor, the binding's first read happened in that untracked window, so it stayed reactive only by accident: when the read returned another accessor, insert re-read it and picked up the dependency. A<Loading>answering a still-pending streamed fragment returns its fallback nodes instead, so the effect ended up with no dependency at all and the range went permanently inert. The boundary's own resume still claimed the swapped-in server markup — the region looks right — but nothing downstream ever re-rendered it. Instrumented: the slot accessor ran once, at claim time, and never again; a route change out of the note produced zero further evaluations.The claim now wraps the
insertcall. The first evaluation is the render effect's own synchronous compute — still under the producer's keys, but tracked.Tests
frames-late-boundary-client: a held fragment still owes its element afterdone(asserting the follow-up call actually reaches the network — the user-visible symptom), plus the give-up path when no deferred fragment remains.test/hydration/adopted-slot-live.spec.tsx: the live slot range. It lives in the hydrate config becauseclaimRenderbails out entirely withoutsharedConfig.getNextContextId, so the untracked window never opens in the plain client config — hence the one alias line added tovite.config.hydrate.mjs.All three fail on the prior code. Suites green: 340 client, 77 hydrate, 201 server.
Verified end to end in the notes example (dev and a production build, fresh origins, real clicks):
/notes/0→ other notes each fetch and morph, and New/Edit/home all navigate and render.🤖 Generated with Claude Code